Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 329d1df1aa8bd123d6170ef0e5b3fc54a1fe07fd


Parents : 76cb936
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-13T18:46:51-05:00

feat(tests): update ConversationViewer tests with clipboard handling, file attachment functionality, and message translation

Changes

2 files changed, 207 insertions(+), 1 deletions(-)


Diff

diff --git a/tests/frontend/ConversationViewer.test.js b/tests/frontend/ConversationViewer.test.js
index e413acbb..e317f714 100644
--- a/tests/frontend/ConversationViewer.test.js
+++ b/tests/frontend/ConversationViewer.test.js
@@ -105,6 +105,72 @@ describe("ConversationViewer.vue", () => {
expect(wrapper.vm.newMessageImages).toHaveLength(1);
});
+ it("onMessagePaste ignores non-image clipboard files (e.g. PDF) and does not prevent default", () => {
+ const wrapper = mountConversationViewer();
+ const file = new File([""], "doc.pdf", { type: "application/pdf" });
+ const event = {
+ preventDefault: vi.fn(),
+ clipboardData: {
+ items: [
+ {
+ kind: "file",
+ type: "application/pdf",
+ getAsFile: () => file,
+ },
+ ],
+ },
+ };
+ wrapper.vm.onMessagePaste(event);
+ expect(event.preventDefault).not.toHaveBeenCalled();
+ expect(wrapper.vm.newMessageImages).toHaveLength(0);
+ });
+
+ it("onMessagePaste does nothing when clipboard has no image file items", () => {
+ const wrapper = mountConversationViewer();
+ const event = {
+ preventDefault: vi.fn(),
+ clipboardData: {
+ items: [{ kind: "string", type: "text/plain", getAsString: () => "hi" }],
+ },
+ };
+ wrapper.vm.onMessagePaste(event);
+ expect(event.preventDefault).not.toHaveBeenCalled();
+ expect(wrapper.vm.newMessageImages).toHaveLength(0);
+ });
+
+ it("onMessagePaste adds multiple images from a single paste event", () => {
+ const wrapper = mountConversationViewer();
+ const f1 = new File([""], "a.png", { type: "image/png" });
+ const f2 = new File([""], "b.png", { type: "image/png" });
+ const event = {
+ preventDefault: vi.fn(),
+ clipboardData: {
+ items: [
+ { kind: "file", type: "image/png", getAsFile: () => f1 },
+ { kind: "file", type: "image/png", getAsFile: () => f2 },
+ ],
+ },
+ };
+ wrapper.vm.onMessagePaste(event);
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(wrapper.vm.newMessageImages).toHaveLength(2);
+ });
+
+ it("pasteFromClipboard inserts text at the message input selection", async () => {
+ const readText = vi.fn(() => Promise.resolve("pasted-text"));
+ vi.stubGlobal("navigator", {
+ ...navigator,
+ clipboard: { readText },
+ });
+ const wrapper = mountConversationViewer();
+ const ta = wrapper.find("#message-input").element;
+ ta.selectionStart = 0;
+ ta.selectionEnd = 0;
+ wrapper.vm.newMessageText = "";
+ await wrapper.vm.pasteFromClipboard();
+ expect(wrapper.vm.newMessageText).toBe("pasted-text");
+ });
+
it("adds multiple images and renders previews", async () => {
const wrapper = mountConversationViewer();

diff --git a/tests/frontend/ConversationViewerButtons.test.js b/tests/frontend/ConversationViewerButtons.test.js
index 15ce199e..1f2ddcaa 100644
--- a/tests/frontend/ConversationViewerButtons.test.js
+++ b/tests/frontend/ConversationViewerButtons.test.js
@@ -15,6 +15,15 @@ vi.mock("@/js/DialogUtils", () => ({
},
}));
+vi.mock("@/js/ToastUtils", () => ({
+ default: {
+ success: vi.fn(),
+ error: vi.fn(),
+ loading: vi.fn(),
+ info: vi.fn(),
+ },
+}));
+
describe("ConversationViewer.vue button interactions", () => {
let axiosMock;
@@ -68,7 +77,8 @@ describe("ConversationViewer.vue button interactions", () => {
...overrides,
},
global: {
- mocks: { $t: (key) => key },
+ directives: { "click-outside": { mounted: () => {}, unmounted: () => {} } },
+ mocks: { $t: (key) => key, $i18n: { locale: "en" } },
stubs: {
MaterialDesignIcon: true,
AddImageButton: true,
@@ -216,4 +226,134 @@ describe("ConversationViewer.vue button interactions", () => {
expect(typeof wrapper.vm.openShareContactModal).toBe("function");
wrapper.vm.openShareContactModal();
});
+
+ describe("compose area: clipboard, file attachments, and toolbar actions", () => {
+ it("add files button triggers the hidden file input click", async () => {
+ const wrapper = mountViewer();
+ await wrapper.vm.$nextTick();
+ const fileInput = wrapper.find('input[type="file"]');
+ const clickSpy = vi.spyOn(fileInput.element, "click").mockImplementation(() => {});
+
+ const actionButtons = wrapper.findAll(".attachment-action-button");
+ await actionButtons[0].trigger("click");
+
+ expect(clickSpy).toHaveBeenCalled();
+ clickSpy.mockRestore();
+ });
+
+ it("onFileInputChange appends selected files to newMessageFiles", () => {
+ const wrapper = mountViewer();
+ const f = new File(["x"], "attach.txt", { type: "text/plain" });
+ wrapper.vm.onFileInputChange({ target: { files: [f] } });
+ expect(wrapper.vm.newMessageFiles).toContain(f);
+ });
+
+ it("removeFileAttachment removes one file from newMessageFiles", () => {
+ const wrapper = mountViewer();
+ const f1 = new File(["a"], "a.txt", { type: "text/plain" });
+ const f2 = new File(["b"], "b.txt", { type: "text/plain" });
+ wrapper.vm.newMessageFiles = [f1, f2];
+ wrapper.vm.removeFileAttachment(f1);
+ expect(wrapper.vm.newMessageFiles).toEqual([f2]);
+ });
+
+ it("paste toolbar button reads clipboard text into the message field", async () => {
+ const readText = vi.fn(() => Promise.resolve("from-clipboard"));
+ vi.stubGlobal("navigator", {
+ ...navigator,
+ clipboard: { readText },
+ });
+ const wrapper = mountViewer();
+ await wrapper.vm.$nextTick();
+ const ta = wrapper.find("#message-input").element;
+ ta.selectionStart = 0;
+ ta.selectionEnd = 0;
+ wrapper.vm.newMessageText = "";
+
+ const actionButtons = wrapper.findAll(".attachment-action-button");
+ await actionButtons[1].trigger("click");
+ await vi.waitFor(() => expect(wrapper.vm.newMessageText).toBe("from-clipboard"));
+ });
+
+ it("translateMessage replaces text when the translator API succeeds", async () => {
+ axiosMock.post.mockImplementation((url) => {
+ if (url.includes("/translator/translate")) {
+ return Promise.resolve({ data: { translated_text: "translated" } });
+ }
+ return Promise.resolve({ data: {} });
+ });
+ const wrapper = mountViewer();
+ wrapper.vm.newMessageText = "hello";
+ await wrapper.vm.translateMessage();
+ expect(wrapper.vm.newMessageText).toBe("translated");
+ expect(axiosMock.post).toHaveBeenCalledWith(
+ "/api/v1/translator/translate",
+ expect.objectContaining({
+ text: "hello",
+ target_lang: "en",
+ })
+ );
+ });
+
+ it("generatePaperMessageFromComposition sends lxm.generate_paper_uri over the websocket", async () => {
+ const sendSpy = vi.spyOn(WebSocketConnection, "send").mockImplementation(() => {});
+ const wrapper = mountViewer();
+ wrapper.vm.newMessageText = "paper body";
+ await wrapper.vm.generatePaperMessageFromComposition();
+ expect(sendSpy).toHaveBeenCalled();
+ const payload = JSON.parse(sendSpy.mock.calls[0][0]);
+ expect(payload.type).toBe("lxm.generate_paper_uri");
+ expect(payload.destination_hash).toBe("a".repeat(32));
+ expect(payload.content).toBe("paper body");
+ sendSpy.mockRestore();
+ });
+
+ it("requestLocation posts a telemetry request command", async () => {
+ const wrapper = mountViewer();
+ await wrapper.vm.requestLocation();
+ expect(axiosMock.post).toHaveBeenCalledWith(
+ "/api/v1/lxmf-messages/send",
+ expect.objectContaining({
+ lxmf_message: expect.objectContaining({
+ destination_hash: "a".repeat(32),
+ fields: {
+ commands: [{ "0x01": expect.any(Number) }],
+ },
+ }),
+ })
+ );
+ });
+
+ it("shareLocation with manual coordinates sets telemetry and calls sendMessage", async () => {
+ const wrapper = mountViewer({
+ config: {
+ location_source: "manual",
+ location_manual_lat: "12.34",
+ location_manual_lon: "56.78",
+ location_manual_alt: "9",
+ },
+ });
+ const sendSpy = vi.spyOn(wrapper.vm, "sendMessage").mockResolvedValue(undefined);
+ await wrapper.vm.shareLocation();
+ expect(sendSpy).toHaveBeenCalled();
+ expect(wrapper.vm.newMessageTelemetry).toMatchObject({
+ latitude: 12.34,
+ longitude: 56.78,
+ altitude: 9,
+ });
+ sendSpy.mockRestore();
+ });
+
+ it("openConversationPopout opens a popout URL with the peer hash", () => {
+ const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
+ const wrapper = mountViewer();
+ wrapper.vm.openConversationPopout();
+ expect(openSpy).toHaveBeenCalledWith(
+ expect.stringContaining(encodeURIComponent("a".repeat(32))),
+ "_blank",
+ expect.stringContaining("width=")
+ );
+ openSpy.mockRestore();
+ });
+ });
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────